spring-io / initializr

A quickstart generator for Spring projects
https://start.spring.io
Apache License 2.0
3.46k stars 1.74k forks source link

Exclusion of ProjectGenerationConfiguration #1581

Open zambrovski opened 6 days ago

zambrovski commented 6 days ago

Hi folks,

we are using initializr to create larger enterprise-wide setup and are extending it in several directions.

One of the important things that we observed was the inability to exclude ProjectGenerationConfiguration from being processed. This is a little problematic, because once on the classpath, they are loaded via Spring SPI Loader. And since the initializr-spring module provides many useful things, we wanted to be able to disable configuration explicitly.

Do you think this is a good idea?

We implemented the following solution and would like to contribute it back, if you think it makes sense. In general, we managed to subclass the ProjectGenerator and overwrite the method getCandidateProjectGenerationConfigurations. In our implementation, we filter the detected candidates by comparing them with another list, beefore returning:

protected List<String> getCandidateProjectGenerationConfigurations(ProjectDescription description) {
    var excludedNames = SpringFactoriesLoader.loadFactoryNames(
        ExcludedProjectGenerationConfiguration.class, getClass().getClassLoader()
    );
    log.trace("Found excluded configurations: {}", String.join(", ", excludedNames));
    return super.getCandidateProjectGenerationConfigurations(description)
        .stream()
        .filter(candidate -> !excludedNames.contains(candidate))
        .toList();
}

By doing so, I can specify in spring.factories:

io.spring.initializr.generator.project.ExcludedProjectGenerationConfiguration=\
  io.spring.initializr.generator.spring.code.java.JavaProjectGenerationConfiguration, \  
io.spring.initializr.generator.spring.configuration.ApplicationConfigurationProjectGenerationConfiguration

and exclude those two generation configurations, ExcludedProjectGenerationConfiguration being just a marker annotation and not effectively doing anything else.

In general, by providing this small change, we would benefit of throwing away a lot of code related to replacement of the ProjectGeneratorInvokerand the surronding ecosystem (because it is not very easy to replace the ProjectGenerator creation / invocation).

Are you interested?

We would love to contribute back.

mhalbritter commented 4 days ago

Hey,

thanks for the idea. I think that can be useful. However, I don't like the proposed implementation. I'd think it would make sense to add a new interface:

/**
 * Allows to vote against the inclusion of a {@link ProjectGenerationConfiguration}.
 *
 * @author Moritz Halbritter
 */
public interface ProjectGenerationConfigurationVetoer {

    /**
     * Whether the given configuration class should be excluded.
     * @param configurationClass the configuration class
     * @return whether the configuration class should be excluded
     */
    boolean exclude(Class<?> configurationClass);

}

where implementations can be registered via spring.factories. This interface is used in the ProjectGenerator to exclude configurations:

    /**
     * Return the {@link ProjectGenerationConfiguration} class names that should be
     * considered. By default this method will load candidates using
     * {@link SpringFactoriesLoader} with {@link ProjectGenerationConfiguration}.
     * {@link ProjectGenerationConfigurationVetoer} loaded using
     * {@link SpringFactoriesLoader} can be used to veto the loading of a
     * {@link ProjectGenerationConfiguration}. If one vetoer vetoes againsg the
     * configuration, the configuration is excluded.
     * @param description the description of the project to generate
     * @return a list of candidate configurations
     */
    @SuppressWarnings("deprecation")
    protected List<String> getCandidateProjectGenerationConfigurations(ProjectDescription description) {
        List<String> candidates = SpringFactoriesLoader.loadFactoryNames(ProjectGenerationConfiguration.class,
                getClass().getClassLoader());
        ProjectGenerationConfigurationVetoer vetoer = getProjectGenerationConfigurationVetoer();
        List<String> result = new ArrayList<>(candidates);
        for (String candidate : candidates) {
            Class<?> configurationClass = resolveClass(candidate);
            if (configurationClass != null) {
                if (vetoer.exclude(configurationClass)) {
                    result.remove(candidate);
                }
            }
        }
        return result;
    }

I've prepared something in https://github.com/mhalbritter/initializr/tree/mh/1581-exclusion-of-projectgenerationconfiguration

Would that suit your needs?

zambrovski commented 4 days ago

Hi Moritz,

thank you for the response. I had a look on your proposal.

If I get it right, you prefer the type-based exclusion instead of solution proposed by me. By doing so, you exclude the configurations if there is at least one instance of ProjectGenerationConfigurationVetoer registered via spring.factories that answers true for configuration. So if I want to exclude a number of configurations, I would implement a ProjectGenerationConfigurationVetoer, register it and this would imperative (instead of declarative) rejects the configurations that I need to exclude.

This sounds like a good solution and solves my problem. I would like to exclude some "own" configurations and some of the configurations provided by the spring-initializer as default.

P.S. In this case you provided the solutions, but next time I would be happy to contribute code / PRs, if you agree on the feature.

P.P.S We are building a multi-module Maven Build setup (replacing the BuildSystem, introducing modules, and allowing generation of more elaborate setups). As I was replacing the ProjectGenerator and ProjectGeneratorInvoker I found some small inaccuracies and would just create a PR with few minor corrections. They are all non-critical but I'll still report it to you.

Thanks and cheers,

Simon

mhalbritter commented 2 days ago

Hey Simon,

you got that right.

However, i've done more thinking on the feature, and while I still like my "strategy interface" idea, I now lean towards implementing it differently. Instead of calling that thing "vetoer" which can veto the inclusion, i'd go for Filter (more matching org.springframework.boot.context.TypeExcludeFilter or org.springframework.core.type.filter.TypeFilter) and use a match method, which returns true if the configuration should be excluded (which would be the same as the behavior for org.springframework.context.annotation.ComponentScan#excludeFilters).

If you're up to it, you could implement that idea, feel free to use the ideas in my branch if you like.

and would just create a PR with few minor corrections.

Sure, go for it! Thanks!

zambrovski commented 20 hours ago

Sure, I'm on it...