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

Picocli does not inject options if a bean is annotated with @Transactional #1650

Closed
pluess opened this issue Apr 5, 2022 · 17 comments
Closed
Labels
theme: integration An issue or change related to integration with other frameworks, shells or operating systems type: doc 📘
Milestone

Comments

@pluess
Copy link

pluess commented Apr 5, 2022

I have spring bean which is represents a command and uses @transactional for DB access.

@Service
@Command(name="datenlieferantLoeschen")
@Transactional
public class DatenlieferantLoeschen implements Callable<Integer> {

    @Option(names = {"-d", "--dlids", "--datenLieferantIds"}, required = true)
    long[] datenliefrantIds;

    @Option(names = {"-dr", "--dryRun"})
    boolean dryRun;

    @Spec
    CommandLine.Model.CommandSpec spec;

    @Override
    public Integer call() {
        // ...
    }
    
    // ...
}

Up on entering the call() method the options nor spec is set.
While debugging I realized, that the object picocli is using to inject the options and the spec and the one finally executing the call() method within the spring managed transaction are not identical.

As a workaround I can of course use springs TransactionTemplate.

@remkop
Copy link
Owner

remkop commented Apr 5, 2022

The picocli-related code you’re showing looks fine.

How is this command wired up to the main method of the application? I think that’s where the problem may be…

@remkop
Copy link
Owner

remkop commented Apr 5, 2022

Actually, I don’t know what @Transactional does. Does Spring instantiate objects marked with that annotation? Because that may result in the problem you describe.

@pluess
Copy link
Author

pluess commented Apr 5, 2022

@transactional wraps a DB-Transaction around all public methods (if the caller is also a spring bean). @transactional can also be used on selected methods only. I tried this, but had the same problems.

When I remove the annotation and user programmatic transaction handling, everything works fine.

This is my main Class:

@SpringBootApplication(scanBasePackages = {" ... "})
@EntityScan(" ... ")
@EnableJpaRepositories({" ... "})
public class ApplicationDbChanges {
 
    public static void main(String[] args) {
        SpringApplication.run(ApplicationDbChanges.class, args);
    }

}

MainCommand.java:

@Component
@Command(
    name="dbchange",
    subcommands = {DatenlieferantLoeschen.class, ...   },
)
public class MainCommand implements Callable<Integer> {
    // ...
}

And the CommandLineRunner implementation:

@Component
public class DbChangesCommandLineRunner implements CommandLineRunner, ExitCodeGenerator {

    private final MainCommand mainCommand;
    private final IFactory factory; // auto-configured to inject PicocliSpringFactory
    private int exitCode;

    public DbChangesCommandLineRunner(
        MainCommand mainCommand, IFactory factory
    ) {
        this.mainCommand = mainCommand;
        this.factory = factory;
    }

    @Override
    public void run(String... args) {
        exitCode = new CommandLine(mainCommand, factory).execute(args);
    }

    @Override
    public int getExitCode() {
        return exitCode;
    }
}

@remkop remkop added the theme: integration An issue or change related to integration with other frameworks, shells or operating systems label Apr 5, 2022
@remkop
Copy link
Owner

remkop commented Apr 6, 2022

Would it be an idea to put the @Transactional-annotated class/methods in a separate class from the picocli command class?

For example:

@Command(name="datenlieferantLoeschen")
public class DatenlieferantLoeschen implements Callable<Integer> {

    @Inject // or @Autowired
    private MyBusinessLogic myBusinessLogic; // injected by Spring

    @Option(names = {"-d", "--dlids", "--datenLieferantIds"}, required = true)
    long[] datenliefrantIds;

    @Option(names = {"-r", "--dryRun"})
    boolean dryRun;

    @Spec
    CommandLine.Model.CommandSpec spec;

    @Override
    public Integer call() {
        myBusinessLogic.doStuff();
    }
}

@Service
@Transactional
class MyBusinessLogic {
    public void doStuff() {
        //...
    }
}

@pluess
Copy link
Author

pluess commented Apr 6, 2022

Yes, everything that keeps the spring transaction management away from the command bean works.

But there is definitely something wrong. It's likely that any kind of crossfunctional behaviour mixed in by spring will cause the same problem.

@pluess
Copy link
Author

pluess commented Apr 6, 2022

Just a guess:
To me it looks like picocli is injecting the values directly into the fields of the object.
In a spring application this does not work reliably, because you never know whether you get just a proxy or the original object.

@remkop
Copy link
Owner

remkop commented Apr 6, 2022

Ok, thanks for confirming that that solution works!
What shall we do to help other users?

We can add a note to the picocli user manual Spring section saying something like:

Note that some Spring annotations like @Transactional cannot be used on picocli @Command-annotated classes: Spring will generate a new proxy instance for each call to a public method in these classes, and the information injected into @Option and @Parameters-annotated fields will not be present in those proxy instances. Please use separate classes for @Transaction-related logic.

Additionally, it may be an idea to add logic to the picocli annotation processor (the picocli-codegen module) to detect @org.springframework.transaction.annotation.Transactional annotations on picocli-annotated classes or methods of picocli-annotated classes and throw a compile-time error with a similar message about this annotation being incompatible.

Also, does Spring have a mechanism to declare some annotations incompatible with each other? If so, perhaps that mechanism can be used in the picocli-spring-boot-starter module to declare this incompatibility.

Thoughts?

@pluess
Copy link
Author

pluess commented Apr 7, 2022

As long picocli cannot deal with this, any kind of documentation and hints is useful. It would have saved me a full day of nasty debugging.

It's the purpose of CDI framworks like spring to allow for any number of cross cutting concerns to be mixed in at runtime. So you can easily have transactioning, security, logging and caching. There are a lot of 3rd party framworks (like picocli :-) ) that provide spring integration like this. It's impossible to maintain a list of incompatible tags.

To be fully spring compatible, picocli should use setter methods to inject values in favour of direct field injection. It's likely that this is also true for other CDI frameworks.

@remkop
Copy link
Owner

remkop commented Apr 7, 2022

As long picocli cannot deal with this, any kind of documentation and hints is useful.

I’m still unclear on how picocli can be made to deal with this. 😅
(Other than rejecting this combination of annotations at compile time or at runtime.)

I can certainly add the documentation mentioned above (any suggestions to improve the wording are welcome).

To be fully spring compatible, picocli should use setter methods to inject values in favour of direct field injection. It's likely that this is also true for other CDI frameworks.

Oh, would that solve the problem?
Picocli already supports @Option and @Parameters-annotated setter methods (see https://picocli.info/#option-parameters-methods). Can you try this?
But I doubt that this would help if Spring replaces the instance where these values are stored with another instance that doesn’t have these values….

@pluess
Copy link
Author

pluess commented Apr 7, 2022

My bad, I searched for something like that but missed this in the docs.

Yes, for CDI containers like spring, using setter methods is a must and should always work. If the container wrapps the original object, it is also responsible to delegate to the method on the original object. So this will always work.

I'm not an expert on the other CDI containers mentioned in https://picocli.info/#_dependency_injection. But it's likely that they have the same restriction.

I suggest to add something like this in the docs:
CDI containers do wrap managed instances to facilitate AOP. All classes annotated with @Command must use public or package private option parameter setters to make sure value injection is delegated to the managed instances.

I just tested this, and it definitely solves the problem.

@remkop remkop added this to the 4.7 milestone Apr 7, 2022
@remkop
Copy link
Owner

remkop commented Apr 7, 2022

@pluess Great, thank you for testing and confirming this.

Quick question: is just the @Option or @Parameters-annotated setter method enough, or do these classes also need to have public/package protected getter methods?

About the docs phrasing, I'm thinking to keep it limited to the Spring section for now until I can confirm how the same issue would manifest in other frameworks.
The existing Spring example in the picocli manual uses field annotations for options and positional parameters, and that is okay for that example. (I'm not thinking to modify that example to use setters.)

This is what I thought of so far:

IMPORTANT: Some Spring features like @Transactional, Caching and Spring Aspect Oriented Programming (AOP) internally use dynamic proxies. Take care to use @Option and @Parameters-annotated setter methods when using these features in picocli-annotated classes. Using picocli annotations on fields may result in option and positional parameter values not being available in the proxied instance used in the transaction, caching or other AOP operation.

@pluess
Copy link
Author

pluess commented Apr 7, 2022

Basically every access to spring beans has to go through public or package private setters methods.

This means the example of MailCommand is not valid spring code. Depending on the dependency graph of the spring beans you can get proxys even if you only use the basic @component annotation.

BTW: All of the above is also true for the @SPEC annotation as well.

@remkop
Copy link
Owner

remkop commented Apr 8, 2022

I see your point, although I would not go as far as saying that the example MailCommand is not valid Spring code. (The example was reviewed and updated by @snicoll, so I have some confidence that it is okay as it stands.)

But your point is valid: with a different configuration, Spring may use proxies and annotated fields will no longer work as expected.

The Spring reference manual examples mostly use constructor and setter method injection. On the other hand, the docs on @Autowired and @Resource explicitly say that field injection is supported, so even the Spring folks seem unwilling to say "never use annotated fields".

So I am still pondering the best way to phrase this... 😅 This is my latest attempt:

IMPORTANT: Depending on your configuration, Spring may use dynamic proxies for managed instances. This is definitely the case when using features like @Transactional, Caching and Spring Aspect Oriented Programming (AOP), but may happen in other configurations also.

The safest thing to do in Spring applications is to use public annotated setter methods for picocli's @Option and @Parameters, @Spec, and other annotations. Using picocli annotations on fields or on non-public methods may result in picocli-injected values not being available in the proxied instance.

@pluess
Copy link
Author

pluess commented Apr 8, 2022

That's perfect.

@remkop remkop closed this as completed in 52f6886 Apr 9, 2022
@remkop
Copy link
Owner

remkop commented Apr 9, 2022

I added the above text to the user manual.
Many thanks for raising this and helping with the analysis and documentation improvement!

@Mert-Z
Copy link

Mert-Z commented May 21, 2024

@remkop I hit this same issue on Weld SE CDI following the example in the manual: https://picocli.info/#_cdi_2_0_jsr_365

In that example, those fields having @option annotation are not getting set to the provided values due to picocli setting the fields on a proxy which is retrieved from CDI. I only managed to get the fields populated with correct values after creating public setter methods and annotating them with @option.

I believe manual needs update to reflect this. If you agree, I'm happy to raise a PR if you can point me to the source of the manual.

@remkop
Copy link
Owner

remkop commented May 21, 2024

@remkop I hit this same issue on Weld SE CDI following the example in the manual: https://picocli.info/#_cdi_2_0_jsr_365

In that example, those fields having @option annotation are not getting set to the provided values due to picocli setting the fields on a proxy which is retrieved from CDI. I only managed to get the fields populated with correct values after creating public setter methods and annotating them with @option.

I believe manual needs update to reflect this. If you agree, I'm happy to raise a PR if you can point me to the source of the manual.

@Mert-Z
Thank you for noticing and offering to create a PR!
The source of the manual is in docs/index.adoc.

The html is generated by the build. (From memory, running gradlew doc will just run the part of the build that generates the documentation into build/generated-docs.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: integration An issue or change related to integration with other frameworks, shells or operating systems type: doc 📘
Projects
None yet
Development

No branches or pull requests

3 participants